package com.luych.toolbox.base.service.service;

import com.luych.toolbox.base.service.constant.Constant;
import com.luych.toolbox.base.service.dao.ScriptArgDao;
import com.luych.toolbox.base.service.dao.ScriptDao;
import com.luych.toolbox.base.service.entity.Script;
import com.luych.toolbox.base.service.entity.ScriptArg;
import com.luych.toolbox.common.event.base.service.publisher.WebSocketEventPublisher;
import com.luych.toolbox.common.feign.param.base.service.ScriptArgParam;
import com.luych.toolbox.common.feign.param.base.service.ScriptParam;
import com.luych.toolbox.common.feign.param.gateway.center.WebSocketEvent;
import com.luych.toolbox.common.feign.view.base.service.ScriptArgView;
import com.luych.toolbox.common.feign.view.base.service.ScriptView;
import com.luych.toolbox.common.page.PageParam;
import com.luych.toolbox.common.page.PageResult;
import com.luych.toolbox.common.service.base.BaseConvert;
import com.luych.toolbox.common.service.base.BaseService;
import com.luych.toolbox.common.service.exception.HttpException;
import com.luych.toolbox.common.service.page.SpringPageParamAdapter;
import com.luych.toolbox.common.service.page.SpringPageResultAdapter;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.cloud.bus.BusProperties;
import org.springframework.context.ApplicationEventPublisher;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.data.jpa.domain.Specification;
import org.springframework.http.HttpStatus;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.util.Assert;
import org.springframework.util.StringUtils;

import javax.persistence.criteria.Predicate;
import java.util.ArrayList;
import java.util.List;
import java.util.Optional;
import java.util.UUID;
import java.util.stream.Collectors;

@Service
public class ScriptService extends BaseService<ScriptView, ScriptParam, Script> {

    private static final BaseConvert<ScriptParam, Script> scriptParamToEntity = Script::new;
    private static final BaseConvert<Script, ScriptView> scriptEntityToView = ScriptView::new;
    private static final BaseConvert<ScriptArgParam, ScriptArg> scriptArgParamToEntity = ScriptArg::new;
    private static final BaseConvert<ScriptArg, ScriptArgView> scriptArgEntityToView = ScriptArgView::new;

    private final ScriptDao scriptDao;
    private final ScriptArgDao scriptArgDao;

    private final BusProperties busProperties;
    private final ApplicationEventPublisher applicationEventPublisher;

    @Autowired
    public ScriptService(ScriptDao scriptDao, ScriptArgDao scriptArgDao, BusProperties busProperties, ApplicationEventPublisher applicationEventPublisher) {
        super(scriptDao, scriptParamToEntity, scriptEntityToView);
        this.scriptDao = scriptDao;
        this.scriptArgDao = scriptArgDao;
        this.busProperties = busProperties;
        this.applicationEventPublisher = applicationEventPublisher;
    }

    @Override
    protected ScriptView fill(ScriptView view) {
        view.setArgs(scriptArgDao.findAllByScriptId(view.getId()).stream().map(scriptArgEntityToView).collect(Collectors.toList()));
        view.sortArgs();
        return view;
    }

    public PageResult<ScriptView> pageByKeyword(String keyword, PageParam pageParam) {
        Specification<Script> specification = (root, query, criteriaBuilder) -> {
            List<Predicate> predicates = new ArrayList<>();
            if (!StringUtils.isEmpty(keyword)) {
                Predicate nameLike = criteriaBuilder.like(root.get("name"), '%' + keyword + '%');
                Predicate textLike = criteriaBuilder.like(root.get("text"), '%' + keyword + '%');
                Predicate keywordLike = criteriaBuilder.or(nameLike, textLike);
                predicates.add(keywordLike);
            }
            return criteriaBuilder.and(predicates.toArray(new Predicate[0]));
        };

        Pageable pageable = new SpringPageParamAdapter(pageParam);
        Page<Script> page = scriptDao.findAll(specification, pageable);
        PageResult<Script> pageResult = new SpringPageResultAdapter<>(page);
        return pageResult.convert(scriptEntityToView).convert(this::fill);
    }

    @Override
    @Transactional
    public ScriptView create(ScriptParam param) {
        Assert.notNull(param.getName(), "Param name can't be empty");
        Assert.notNull(param.getType(), "Param type can't be empty");
        Assert.notNull(param.getText(), "Param text can't be empty");

        WebSocketEventPublisher eventPublisher = new WebSocketEventPublisher(busProperties, applicationEventPublisher);

        // check name exists
        if (scriptDao.findByName(param.getName()).isPresent()) {
            throw new HttpException(HttpStatus.CONFLICT, Constant.SCRIPT_SAVE_NAME_USED);
        }
        // save script
        ScriptView view = super.create(param);
        // save script args
        if (param.getArgs() != null) {
            List<ScriptArgView> argViews = new ArrayList<>();
            for (ScriptArgParam argParam : param.getArgs()) {
                if (StringUtils.isEmpty(argParam.getId())) {
                    argParam.setId(UUID.randomUUID().toString());
                }
                ScriptArg argEntity = scriptArgParamToEntity.apply(argParam);
                argEntity.setScriptId(view.getId());
                ScriptArg argResult = scriptArgDao.save(argEntity);
                ScriptArgView argView = scriptArgEntityToView.apply(argResult);
                argViews.add(argView);
            }
            view.setArgs(argViews);
        }

        eventPublisher.send(new WebSocketEvent(WebSocketEvent.Type.SCRIPT_REFRESH, "CREATE"));
        return view;
    }

    @Override
    @Transactional
    public ScriptView modify(ScriptParam param) {
        Assert.notNull(param.getId(), "Param id can't be empty");
        Assert.notNull(param.getName(), "Param name can't be empty");
        Assert.notNull(param.getType(), "Param type can't be empty");
        Assert.notNull(param.getText(), "Param text can't be empty");

        WebSocketEventPublisher eventPublisher = new WebSocketEventPublisher(busProperties, applicationEventPublisher);

        Script oldScript = scriptDao.findById(param.getId()).orElseThrow(() ->
                new HttpException(HttpStatus.NOT_FOUND, "Can't find Script with id: " + param.getId()));

        // check name exists
        Optional<Script> scriptOptOfName = scriptDao.findByName(param.getName());
        if (scriptOptOfName.isPresent() && !scriptOptOfName.get().getId().equals(param.getId())) {
            throw new HttpException(HttpStatus.CONFLICT, Constant.SCRIPT_SAVE_NAME_USED);
        }

        // save script
        ScriptView view = super.modify(param);
        // save script args
        if (param.getArgs() != null) {
            scriptArgDao.removeByScriptId(view.getId());

            List<ScriptArgView> argViews = new ArrayList<>();
            for (ScriptArgParam argParam : param.getArgs()) {
                if (StringUtils.isEmpty(argParam.getId())) {
                    argParam.setId(UUID.randomUUID().toString());
                }
                ScriptArg argEntity = scriptArgParamToEntity.apply(argParam);
                argEntity.setScriptId(view.getId());
                ScriptArg argResult = scriptArgDao.save(argEntity);
                ScriptArgView argView = scriptArgEntityToView.apply(argResult);
                argViews.add(argView);
            }
            view.setArgs(argViews);
        }

        eventPublisher.send(new WebSocketEvent(WebSocketEvent.Type.SCRIPT_REFRESH, "MODIFY"));
        return view;
    }

    @Override
    @Transactional
    public void remove(String id) {
        scriptArgDao.removeByScriptId(id);
        super.remove(id);
    }
}
